package cc.aliza.lib.mongo.core;

import cc.aliza.lib.mongo.core.annotations.EnsureIndex;
import cc.aliza.lib.mongo.core.annotations.Entity;
import cc.aliza.lib.mongo.core.annotations.Id;
import cc.aliza.lib.mongo.core.annotations.IdType;
import cc.aliza.lib.mongo.core.cache.FieldsCache;
import cc.aliza.lib.mongo.core.listener.AlizaFramework;
import cc.aliza.lib.mongo.core.listener.CascadeChecker;
import cc.aliza.lib.mongo.core.listener.CascadeListener;
import cc.aliza.lib.mongo.core.listener.EntityListener;
import cc.aliza.lib.mongo.core.utils.*;
import com.mongodb.WriteConcern;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.model.CreateCollectionOptions;
import org.bson.Document;
import org.bson.types.ObjectId;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 『给时光以生命·给岁月以文明』
 *
 * @author Zhang Jing (byrdkm17@gmail.com).
 */
public class AlizaDao<T> {

    protected final EntityListener listener;
    protected Class<T> clazz;
    protected MongoCollection<Document> coll;
    protected MongoDatabase db;
    protected WriteConcern concern;
    protected Document keys;
    private Entity entity;
    private String collName;

    public AlizaDao(Class<T> clazz) {
        entity = clazz.getAnnotation(Entity.class);
        collName = MapperUtil.getEntityName(clazz);

        this.clazz = clazz;
        db = AlizaConnection.getInstance().getDB(entity.database());
        concern = db.getWriteConcern();
        initCollection();
        keys = MapperUtil.getKeyFields(clazz);
        if (CascadeChecker.needListener(clazz)) {
            listener = new CascadeListener(clazz);
        } else {
            listener = null;
        }
    }

    public Document getKeyFields() {
        return keys;
    }

    public MongoCollection<Document> getCollection() {
        return coll;
    }

    private void initCollection() {
        if (entity.capped() && !collectionExists()) {
            db.createCollection(collName, new CreateCollectionOptions()
                            .capped(true)
                            .sizeInBytes(entity.capSize())
                            .maxDocuments(entity.capMax())
            );
        }
        coll = db.getCollection(collName);
        initIndex();
    }

    private void initIndex() {
        EnsureIndex ensureIndex = clazz.getAnnotation(EnsureIndex.class);
        if (ensureIndex != null) {
            List<DBIndex> list = MapperUtil.getDBIndex(ensureIndex.value());
            list.stream().forEach(o -> coll.createIndex(o.getKey(), o.getOptions()));
        }
    }

    private boolean collectionExists() {
        MongoIterable<String> it = db.listCollectionNames();
        while (it.iterator().hasNext()) {
            if (collName.equalsIgnoreCase(it.iterator().next())) {
                return true;
            }
        }
        return false;
    }

    public WriteConcern getConcern() {
        return concern;
    }

    public void setConcern(WriteConcern concern) {
        this.concern = concern;
    }

    public Class<T> getEntityClass() {
        return clazz;
    }

    public void notifyInserted(AlizaEntity ent) {
        if (listener != null) {
            AlizaFramework.getInstance().getService().execute(() -> listener.entityInserted(ent));
        }
    }

    public void notifyUpdated(AlizaEntity ent) {
        if (listener != null) {
            AlizaFramework.getInstance().getService().execute(() -> listener.entityUpdated(ent));
        }
    }

    public void notifyDeleted(AlizaEntity ent) {
        if (listener != null) {
            AlizaFramework.getInstance().getService().execute(() -> listener.entityDeleted(ent));
        }
    }

    public T insert(T t) {
        Document document = MapperUtil.toDocument(t);
        document.put("createTime", new Date());
        coll.withWriteConcern(concern).insertOne(document);
        String id = document.getObjectId(Operator.ID).toString();
        AlizaEntity ent = (AlizaEntity) t;
        ent.setId(id);
        notifyInserted(ent);
        return t;
    }

    public List<T> insert(List<T> list) {
        if (list != null && !list.isEmpty()) {
            List<Document> documentList = new ArrayList<>();
            list.forEach(o -> {
                Document document = MapperUtil.toDocument(o);
                document.put("createTime", new Date());
                documentList.add(document);
            });
            coll.withWriteConcern(concern).insertMany(documentList);
            int len = documentList.size();
            for (int i = 0; i < len; i++) {
                String id = documentList.get(i).getObjectId(Operator.ID).toString();
                AlizaEntity ent = (AlizaEntity) list.get(i);
                ent.setId(id);
            }
            list.stream().forEach(o -> notifyInserted((AlizaEntity) o));
        }
        return list;
    }

    public void save(T t) {
        AlizaEntity ent = (AlizaEntity) t;
        if (StringUtil.isEmpty(ent.getId())) {
            insert(t);
        } else {
            Field field = FieldsCache.getInstance().getIdField(clazz);
            Id id = field.getAnnotation(Id.class);
            if (IdType.USER_DEFINE.equals(id.type())) {
                if (exists(Operator.ID, ent.getId())) {
                    doSave(ent);
                } else {
                    insert(t);
                }
            } else {
                doSave(ent);
            }
        }
    }

    private void doSave(AlizaEntity ent) {
        notifyUpdated(ent);
        ent.setUpdateTime(new Date());
        coll.replaceOne(new Document(Operator.ID, new ObjectId(ent.getId())), MapperUtil.toDocument(ent));
    }

    public void drop() {
        List<T> list = findAll();
        list.stream().forEach(this::remove);
        coll.dropIndexes();
        coll.drop();
    }

    public void remove(T t) {
        remove(((AlizaEntity) t).getId());
    }

    public void remove(String id) {
        notifyDeleted((AlizaEntity) findOne(id));
        coll.deleteOne(new Document(Operator.ID, IdUtil.toDbId(clazz, id)));
    }

    public void remove(List<String> idList) {
        List<Object> arr = new ArrayList<>();
        idList.stream().forEach(o -> arr.add(IdUtil.toDbId(clazz, o)));
        Document in = new Document(Operator.IN, arr.toArray());
        removeMany(new Document(Operator.ID, in));
    }

    public void remove(String key, Object value) {
        removeMany(new Document(key, checkSpecialValue(key, value)));
    }

    public void remove(AlizaQuery query) {
        removeMany(query.getCondition());
    }

    private void removeMany(Document filter) {
        MongoCursor<Document> cursor = coll.find(filter).iterator();
        List<T> list = MapperUtil.toList(clazz, cursor);
        list.stream().forEach(o -> notifyDeleted((AlizaEntity) o));
        coll.withWriteConcern(concern).deleteMany(filter);
    }

    public T findOne() {
        return MapperUtil.fromDocument(clazz, coll.find().projection(keys).first());
    }

    public T findOne(String id) {
        return findOne(Operator.ID, IdUtil.toDbId(clazz, id));
    }

    public T findOne(String key, Object value) {
        return query().is(key, value).result();
    }

    public List<T> findAll() {
        return query().results();
    }

    public List<T> findAll(String orderBy) {
        return query().sort(orderBy).results();
    }

    public List<T> findAll(int pageNum, int pageSize) {
        return query().pageNumber(pageNum).pageSize(pageSize).results();
    }

    public List<T> findAll(String orderBy, int pageNum, int pageSize) {
        return query().sort(orderBy).pageNumber(pageNum).pageSize(pageSize).results();
    }

    public List distinct(String key) {
        return distinct(key, query());
    }

    public List distinct(String key, AlizaQuery query) {
        return query.distinct(key);
    }

    public long count() {
        return query().count();
    }

    public long count(String key, Object value) {
        return query().is(key, checkSpecialValue(key, value)).count();
    }

    private Object checkSpecialValue(String key, Object value) {
        Object result = value;
        if (value instanceof AlizaEntity) {
            AlizaEntity ent = (AlizaEntity) value;
            result = ReferenceUtil.toDBRef(ent.getClass(), ent.getId());
        }
        if (value instanceof Document && FieldsCache.getInstance().isEmbed(clazz, key)) {
            result = MapperUtil.toDocument(value);
        }
        return result;
    }

    public boolean exists(String key, Object value) {
        return count(key, value) > 0;
    }

    public boolean exists(String id) {
        return exists(Operator.ID, IdUtil.toDbId(clazz, id));
    }

    public AlizaQuery<T> query() {
        return new AlizaQuery<>(this);
    }

    public AlizaUpdater<T> updater() {
        return new AlizaUpdater<>(this);
    }

    public EntityListener getListener() {
        return listener;
    }
}
